import matplotlib

matplotlib.use('Agg')  # Use Agg backend for non-GUI environments
import pygad  # version = 3.0.0
from flask import Flask, request, jsonify
from flask_cors import CORS
import os
import json

app = Flask(__name__)
app.secret_key = b'_5#y3332323xf'
CORS(app, origins='*')

# 输出当前pygad版本
print('Current PyGad\'s version = ' + pygad.__version__)

weight_popScore = 0.1  # 受众评分-权重系数
weight_tags = 1.0  # 知识点匹配-权重系数
weight_typeRate = 0.2  # 题目类型匹配-权重系数
weight_difficultyRate = 0.3  # 题目难度-权重系数
max_popScore = 5.0
max_tags_len = 5

# 初始数据，确保程序稳定性
# 题目范围（如果有需要，可以转化）
questionRange = [
    {
        'questionId': 8,
        'tags': ['迭代', '递归'],
        'questionType': 0,
        'questionDifficulty': 0
    },
    {
        'questionId': 5,
        'tags': ['DFS', '数学'],
        'questionType': 1,
        'questionDifficulty': 0
    },
    {
        'questionId': 6,
        'tags': ['迭代', 'DFS', '数学'],
        'questionType': 1,
        'questionDifficulty': 0
    },
    {
        'questionId': 7,
        'tags': ['迭代', '递归', '数学'],
        'questionType': 1,
        'questionDifficulty': 1
    },
    {
        'questionId': 55,
        'tags': ['迭代', '递归'],
        'questionType': 0,
        'questionDifficulty': 1
    },
    {
        'questionId': 69,
        'tags': ['数学'],
        'questionType': 1,
        'questionDifficulty': 1
    },
    {
        'questionId': 54,
        'tags': ['迭代', '数学'],
        'questionType': 1,
        'questionDifficulty': 1
    },
    {
        'questionId': 68,
        'tags': ['迭代', 'DFS', '数学'],
        'questionType': 0,
        'questionDifficulty': 2
    },
    {
        'questionId': 139,
        'tags': ['迭代'],
        'questionType': 0,
        'questionDifficulty': 2
    },
    {
        'questionId': 150,
        'tags': ['数学'],
        'questionType': 0,
        'questionDifficulty': 2
    }]
popScore = 3.5
questionNum = 5
needTags = ['递归', 'DFS', 'BFS']
typeRate = [0.4, 0.6]
difficultyRate = [0.2, 0.3, 0.5]


@app.route('/best_questionList_predict', methods=['POST'])
def best_questionList_predict():
    try:
        # 获取传入参数
        data = json.loads(request.get_data())
        print('Current intput data:' + str(data))
        # 设置GA超参数
        global popScore
        popScore = data['popScore']
        global questionNum
        questionNum = data['questionNum']
        global needTags
        needTags = data['needTags']
        global typeRate
        typeRate = data['typeRate']
        global difficultyRate
        difficultyRate = data['difficultyRate']
        global questionRange
        questionRange = [e for e in data['questionRange']]
        msg = valid_request_data()
        if msg != 'ok':
            return jsonify({'code': '406', 'msg': msg, 'data': []})
        else:
            solution = ga_result()
            for i in range(len(questionRange)):
                if type(questionRange[i]) == type(''):
                    questionRange[i] = json.loads(questionRange[i])
            question_list = sorted([questionRange[e]['questionId'] for e in solution])
            return jsonify({'code': '200', 'msg': msg, 'data': question_list})
    except Exception:
        return jsonify({'code': '500', 'msg': '服务器内部错误！请联系管理员', 'data': []})


def valid_request_data():
    if len(needTags) <= 0 or len(needTags) > max_tags_len:
        return "标签长度不合规！"
    if questionNum < 1 or questionNum > len(questionRange):
        return "题目个数不合规！合理范围为：[{},{}]".format(1, len(questionRange))
    if popScore < 0 or popScore > max_popScore:
        return "受众评分不合规！合理范围为：[{},{}]".format(0.0, max_popScore)
    if sum(typeRate) != 1 or len(typeRate) != 2 or typeRate[0] < 0 or typeRate[1] < 0:
        return "题目类型概率分布不合规！"
    if sum(difficultyRate) != 1 or len(difficultyRate) != 3 or difficultyRate[0] < 0 or difficultyRate[1] < 0 or \
            difficultyRate[2] < 0:
        return "题目难度概率分布不合规！"

    return "ok"


def ga_result():
    # pyGad参考链接：https://pygad.readthedocs.io/en/latest/pygad.html#pygad-ga-class
    ga_instance = pygad.GA(
        init_range_low=0,  # 初始种群个体的基因最小值
        init_range_high=len(questionRange) - 1,  # 初始种群个体的基因最大值
        num_generations=50,  # 代数
        num_parents_mating=20,  # 要选择的父母解决方案数量
        parent_selection_type="sss",  # 选择策略（steady_state_selection：稳态选择：保证种群稳定性）
        crossover_type="single_point",  # 交叉策略（单点交叉：适用于二进制编码和实数编码等多种编码方式）
        fitness_func=max_fitness_function,  # 适应度函数
        sol_per_pop=100,  # 每一个种群中个体数量
        num_genes=questionNum,  # 染色体基因的数量
        gene_type=int,  # 基因值的数据类型
        gene_space=range(0, len(questionRange)),  # 基因值的取值范围
        allow_duplicate_genes=False,  # 是否允许染色体中存在重复的值
        mutation_type="random",  # 变异的类型：随机突变
        mutation_percent_genes=5,  # 基因变异的百分比概率
        random_mutation_min_val=0,  # 变异个体的基因最小值
        random_mutation_max_val=len(questionRange) - 1  # 变异个体的基因最大值
    )

    ga_instance.run()
    fig = ga_instance.plot_result()
    fig.savefig('fitness_plot.png')  # Save the plot as an image file

    solution, solution_fitness, solution_idx = ga_instance.best_solution()
    print("Parameters of the best solution : {solution}".format(solution=solution))
    print("Fitness value of the best solution = {solution_fitness}".format(solution_fitness=solution_fitness))
    print("Index of the best solution : {solution_idx}".format(solution_idx=solution_idx))
    return [int(e) for e in solution]


def max_fitness_function(ga_instance, solution, solution_idx):
    # 根据solution索引数组获取题目信息数组
    cur = [questionRange[e] for e in solution]
    for i in range(len(cur)):
        if type(cur[i]) == type(''):
            cur[i] = json.loads(cur[i])

    # 计算题目类型分布并归一化
    type_distribution = calCurTypeRate(cur)

    # 计算题目难度分布并归一化
    difficulty_distribution = calCurDifficultyRate(cur)

    # 计算题目受众分数
    cur_popScore = calCurPopScore(difficulty_distribution)

    # 计算适应度函数值
    fitness = (
            weight_popScore * (abs(popScore - cur_popScore) / popScore) +
            weight_tags * calTagsLoss(cur) +
            weight_typeRate * (abs(typeRate[0] - type_distribution[0]) + abs(typeRate[1] - type_distribution[1])) / 2 +
            weight_difficultyRate * sum([abs(difficultyRate[i] - difficulty_distribution[i]) for i in range(3)]) / 3
    )

    return 1.0 / fitness


def calCurTypeRate(cur):
    type0, type1 = 0, 0
    for e in cur:
        if e['questionType'] == 0:
            type0 += 1
        else:
            type1 += 1
    sum = type0 + type1

    return [1.0 * type0 / sum, 1.0 * type1 / sum]


def calCurDifficultyRate(cur):
    difficulty0, difficulty1, difficulty2 = 0, 0, 0
    for e in cur:
        if e['questionDifficulty'] == 0:
            difficulty0 += 1
        elif e['questionDifficulty'] == 1:
            difficulty1 += 1
        else:
            difficulty2 += 1
    sum = difficulty0 + difficulty1 + difficulty2

    return [1.0 * difficulty0 / sum, 1.0 * difficulty1 / sum, 1.0 * difficulty2 / sum]


def calCurPopScore(difficulty_distribution):
    weight_score = [1, 3, 5]
    total_score = 0
    for e in [0, 1, 2]:
        total_score += weight_score[e] * difficulty_distribution[e]

    return min(max_popScore, total_score)


def calTagsLoss(cur):
    sum = 0
    for e in cur:
        single_loss = len(set(e['tags']).intersection(set(needTags)))
        sum += single_loss

    return 1.0 * sum / len(cur)


if __name__ == '__main__':
    os.chdir(os.path.abspath(os.path.dirname(__file__)))
    app.run(debug=True, host='0.0.0.0', port='9696')
